package Main.TcpBreakNat;

import Main.pojo.ClientInfo;
import Main.pojo.ClientSession;
import Main.pojo.TcpMessage;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.*;

/**
 * PACKAGE_NAME: Main.TcpBreakNat
 * Description:
 * 1、与用户建立长连接
 * 2、返回当前用户系列
 * 3、TCP打洞
 */
public class Center implements Runnable {
    private ServerSocket serverSocket;
    private Map<String, ClientSession> sessionMap;
    private List<ClientInfo> clientInfos;

    public Center() throws IOException {
        // 创建socket
        serverSocket = new ServerSocket();
        serverSocket.setSoTimeout(30 * 60 * 1000);
        // backlog：requested maximum length of the queue of incoming connections.
        System.out.println(Inet4Address.getLocalHost());
        serverSocket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), 9999));
        // 数据结构初始化,必须的部分
        sessionMap = new HashMap<>();
        // 有则增强性能的
        clientInfos = new ArrayList<>(); // 供查询Client用户，存储用户的信息列表
    }

    public static void main(String[] args) throws IOException {
        new Thread(new Center()).start();
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 接收到一个新连接
                Socket socket = serverSocket.accept();
                // 启动线程，专门等待接收其后续信息
                // 构造维护的数据结构
                ClientSession clientSession = new ClientSession();
                clientSession.uuid = UUID.randomUUID().toString();
                ClientInfo clientInfo = new ClientInfo(clientSession.uuid, socket.getInetAddress().getHostName(), socket.getInetAddress().getHostAddress());
                clientInfos.add(clientInfo);
                clientSession.clientInfo = clientInfo;
                clientSession.socket = socket;
                clientSession.objectInputStream = new ObjectInputStream(socket.getInputStream());
                clientSession.objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                System.out.println("新接入用户：" + clientInfo);
                sessionMap.put(clientSession.uuid, clientSession);
                // 维护信息记录完毕
                // 创建属于该Socket的接受信息与处理的线程，注意噢，这时候还没启动啊，。，只是规定了要做的事情
                Thread socketThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            while (clientSession.isActive) { // 一直获取并处理信息
                                TcpMessage tcpMessage = (TcpMessage) clientSession.objectInputStream.readObject();
                                System.out.println(socket.getInetAddress().getHostAddress() + ":" + socket.getPort() + "[" + LocalDateTime.now() + "]：" + tcpMessage);
                                requestHandle(clientSession, tcpMessage);
                            }
                            System.out.println(clientSession.uuid + "结束了信息处理的线程。");
                        } catch (IOException | ClassNotFoundException e) {
                            // 关闭连接
                            try {
                                closeSocket(clientSession);
                            } catch (IOException ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                });
                // 启动信息接受处理程序
                socketThread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 关闭与某的SocketClient的连接
     */
    public void closeSocket(ClientSession clientSession) throws IOException {
        System.out.println("断开与主机的连接：" + clientSession.socket.getInetAddress().getHostAddress() + ":" + clientSession.socket.getPort());
        // 线程标记关闭
        clientSession.isActive = false;
        // socket in out 流关闭
        clientSession.objectOutputStream.close();
        clientSession.objectInputStream.close();
        clientSession.socket.close();
        // 相关数据结构维护
        clientInfos.remove(clientSession.clientInfo);
        sessionMap.remove(clientSession.uuid);

    }

    public void requestHandle(ClientSession clientSession, TcpMessage tcpMessage) throws IOException {
        switch (tcpMessage.code) {
            /*
            request server 收到的信息
            1 获取当前所有已连接客户的信息
            2 请求连接某用户，收到的是目标用户的uuid
            3 中心收到UUID，B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
            4 一条文字消息
             */
            /*
            client 收到的信息
            1001 返回当前所有已连接客户的信息
            1002 有人请求TCP与我连接
             */
            /*
            breakTcp 收到的信息
            2001 中央通知A：B联系上了，它的ip\port，请做好准备
            2002 中央通知B：A已通知，请连接它
            2003 一条文字消息
             */
            case 1: // 获取当前所有已连接客户的信息
                TcpMessage response = new TcpMessage();
                response.code = 1001;
                ClientInfo[] clientInfoArray = new ClientInfo[clientInfos.size()];
                clientInfos.toArray(clientInfoArray);
                response.data = clientInfoArray;
                clientSession.objectOutputStream.writeObject(response);
                clientSession.objectOutputStream.flush();
                break;
            case 2: // 请求连接某用户，收到的是目标用户的uuid
                String Buuid = (String) tcpMessage.data;
                // 记录A外网IP与端口
                // the remote IP address to which this socket is connected
                String AOutsideIP = clientSession.socket.getInetAddress().getHostAddress();
                // the remote port number to which this socket is connected
                int AOutsidePort = clientSession.socket.getPort();
                System.out.println(AOutsideIP + ":" + AOutsidePort + "请求连接：" + Buuid);
                // 通知B
                TcpMessage toB = new TcpMessage();
                toB.code = 1002;
                List<Object> data = new ArrayList<>();
                data.add(clientSession.uuid);
                data.add(AOutsideIP);
                data.add(AOutsidePort);
                toB.data = data;
                // 发送到B
                sessionMap.get(Buuid).objectOutputStream.writeObject(toB);
                sessionMap.get(Buuid).objectOutputStream.flush();
                break;
            case 3:
                /*
                4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                 5、中央记录ip,port。通知A：B联系上了，它的ip\port，请做好准备。通知B：A已通知(通知完后，Center也要关闭与他们的连接)，请连接它
                 */
                String AUUID = (String) tcpMessage.data;
                // 记录B外网IP与端口
                // the remote IP address to which this socket is connected
                String BOutsideIP = clientSession.socket.getInetAddress().getHostAddress();
                // the remote port number to which this socket is connected
                int BOutsidePort = clientSession.socket.getPort();
                // 通知A："B联系上了，它的ip\port，请做好准备"
                TcpMessage toATcpMessage = new TcpMessage();
                toATcpMessage.code = 2001;
                toATcpMessage.msg = "B联系上了，数据有它的ip和port，请做好准备";
                Object[] dataArray = new Object[2];
                dataArray[0] = BOutsideIP;
                dataArray[1] = BOutsidePort;
                toATcpMessage.data = dataArray;
                ClientSession clientSessionA =  sessionMap.get(AUUID);
                clientSessionA.objectOutputStream.writeObject(toATcpMessage);
                clientSessionA.objectOutputStream.flush();
                // 中央与A断开连接
                closeSocket(clientSessionA);
                // 通知B："A已通知，请连接它"
                TcpMessage toBMsg = new TcpMessage();
                toBMsg.code = 2002;
                toBMsg.msg = "A已通知，请500ms后连接它";
                clientSession.objectOutputStream.writeObject(toBMsg);
                clientSession.objectOutputStream.flush();
                // 中央与B断开连接
                closeSocket(clientSession);
                break;
            case 4:
                /*
                连接用户流程：
                1、A从中央获取有那些用户，并且获取常驻通讯
                2、A新建一个Socket与Center建立连接，并告诉中央要连接B，通过B的UUID
                3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：uuid,ip,port,需要与你建立连接。
                4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                5、中央记录ip,port。通知A：B联系上了，它的ip\port，请做好准备。通知B：A已通知(通知完后，Center也要关闭与他们的连接)，请连接它
                6、A先试探的用原端口，请求下B5次，100ms一次（为了让NAR给B留个记录）
                7、A用原端口创建ServerSocket
                8、500ms后，B用指定原端口尝试连接5次，400ms一次，如果还是失败，就用再该端口通过中央告诉原主机。
                9、一般这个时候就已经连接成功，先测试，如果成功就不考虑后面
                 */

        }
    }
}
// 未处理BUG
/*TODO 来呀，互相伤害啊
1、自己申请连接自己问题，其他而言不会有这个问题，对服务器也没影响，可以不管
2、tcp打洞没建立验证机制，一般来说port都是随机指定的。展示不要验证机制
 */